package com.ccc.test.util;

import cn.hutool.core.util.StrUtil;
import com.ccc.test.pojo.IpInfo;
import org.lionsoul.ip2region.xdb.Searcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

/**
 * 根据IP获取行政区工具类
 * 下载ip2region地址：https://gitee.com/lionsoul/ip2region/tree/master/data
 * java集成使用示例：https://gitee.com/lionsoul/ip2region/tree/master/binding/java
 * <p>
 * ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架，10微秒级别的查询效率，提供了众多主流编程语言的 xdb 数据生成和查询客户端实现。
 * <p>
 * Ip2region 特性
 * 1、标准化的数据格式
 * 每个 ip 数据段的 region 信息都固定了格式：国家|区域|省份|城市|ISP，只有中国的数据绝大部分精确到了城市，其他国家部分数据只能定位到国家，后前的选项全部是0。
 * <p>
 * 2、数据去重和压缩
 * xdb 格式生成程序会自动去重和压缩部分数据，默认的全部 IP 数据，生成的 ip2region.xdb 数据库是 11MiB，随着数据的详细度增加数据库的大小也慢慢增大。
 * <p>
 * 3、极速查询响应
 * 即使是完全基于 xdb 文件的查询，单次查询响应时间在十微秒级别，可通过如下两种方式开启内存加速查询：
 * <p>
 * vIndex 索引缓存 ：使用固定的 512KiB 的内存空间缓存 vector index 数据，减少一次 IO 磁盘操作，保持平均查询效率稳定在10-20微秒之间。
 * <p>
 * xdb 整个文件缓存：将整个 xdb 文件全部加载到内存，内存占用等同于 xdb 文件大小，无磁盘 IO 操作，保持微秒级别的查询效率。
 * <p>
 * 4、IP 数据管理框架
 * v2.0 格式的 xdb 支持亿级别的 IP 数据段行数，region 信息也可以完全自定义，例如：你可以在 region 中追加特定业务需求的数据，例如：GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
 *
 * @author caochaochao
 * @version 2.0.0
 * @date 2023/6/14
 */

public class Ip2RegionUtil {
    private static final Logger logger = LoggerFactory.getLogger(Ip2RegionUtil.class);

    private static final String dbPath;

    private static Searcher searcher;

    static {
        URL resource = Thread.currentThread().getContextClassLoader().getResource("ip2region.xdb");
        assert resource != null;
        dbPath = resource.getPath();

        // 1、从 dbPath 加载整个 xdb 到内存。
        byte[] cBuff = new byte[0];
        try {
            cBuff = Searcher.loadContentFromFile(dbPath);
        } catch (Exception e) {
            logger.error("failed to load content from `{}`: {}\n", dbPath, e);
        }

        // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
        try {
            searcher = Searcher.newWithBuffer(cBuff);
        } catch (Exception e) {
            logger.error("failed to create content cached searcher: {}\n", e);
        }

    }

    /**
     * 一. 完全基于文件的查询
     * <p>
     * 备注：并发使用，每个线程需要创建一个独立的 searcher 对象单独使用。
     */
    public static String search2File(String ipStr) {
        // 1、创建 searcher 对象
        Searcher searcher = null;
        try {
            searcher = Searcher.newWithFileOnly(dbPath);
        } catch (IOException e) {
            logger.error("failed to create searcher with `{}`: {}\n", dbPath, e);
            return "";
        }

        // 2、查询
        String region = "";
        try {
            long sTime = System.nanoTime();
            region = searcher.search(ipStr);
            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
            logger.info("{region: {}, ioCount: {}, took: {} μs}\n", region, searcher.getIOCount(), cost);
        } catch (Exception e) {
            logger.error("failed to search({}): {}\n", ipStr, e);
        }

        // 3、关闭资源
        try {
            searcher.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return region;
    }

    /**
     * 二. 缓存 VectorIndex 索引
     * <p>
     * 备注：每个线程需要单独创建一个独立的 Searcher 对象，但是都共享全局的制度 vIndex 缓存。
     */
    public static String search2CacheIndex(String ipStr) {

        // 1、从 dbPath 中预先加载 VectorIndex 缓存，并且把这个得到的数据作为全局变量，后续反复使用。
        byte[] vIndex;
        try {
            vIndex = Searcher.loadVectorIndexFromFile(dbPath);
        } catch (Exception e) {
            logger.error("failed to load vector index from `{}`: {}\n", dbPath, e);
            return "";
        }

        // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
        Searcher searcher;
        try {
            searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
        } catch (Exception e) {
            logger.error("failed to create vectorIndex cached searcher with `{}`: {}\n", dbPath, e);
            return "";
        }

        // 3、查询
        String region = "";
        try {
            long sTime = System.nanoTime();
            region = searcher.search(ipStr);
            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
            logger.info("{region: {}, ioCount: {}, took: {} μs}\n", region, searcher.getIOCount(), cost);
        } catch (Exception e) {
            logger.error("failed to search({}): {}\n", ipStr, e);
        }

        // 4、关闭资源
        try {
            searcher.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return region;
    }

    /**
     * 三. 缓存整个 xdb 数据，在内存中进行查询
     * <p>
     * 备注：并发使用，用整个 xdb 数据缓存创建的查询对象可以安全的用于并发，也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
     */
    public static String search2CacheDB(String ipStr) {

        // 3、查询
        String region = "";
        try {
            long sTime = System.nanoTime();
            region = searcher.search(ipStr);
            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
            logger.info("{region: {}, ioCount: {}, took: {} μs}\n", region, searcher.getIOCount(), cost);
        } catch (Exception e) {
            logger.error("failed to search({}): {}\n", ipStr, e);
        }

        // 4、关闭资源 - 该 searcher 对象可以安全用于并发，等整个服务关闭的时候再关闭 searcher
        // searcher.close();

        return region;
    }

    public static IpInfo searchIpInfo(String ipStr) {
        String region = Ip2RegionUtil.search2CacheDB(ipStr);

        if (StrUtil.isEmpty(region)) {
            return null;
        }

        IpInfo ipInfo = new IpInfo();
        String[] arr = region.split("\\|");
        ipInfo.setIp(ipStr);
        if (arr.length > 0) {
            ipInfo.setCountry(arr[0]);
            ipInfo.setRegion_id(arr[1]);
            ipInfo.setRegion(arr[2]);
            ipInfo.setCity(arr[3]);
            ipInfo.setIsp(arr[4]);
        }

        return ipInfo;
    }
}
